package chrome

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	cmdutils "github.com/MzoroHaHa/dev-tools/backend/app/cmd-utils"
	"github.com/MzoroHaHa/dev-tools/backend/app/logger"
	"golang.org/x/net/websocket"
	"log/slog"
	"math/rand"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

type Target struct {
	targetInfo            *TargetInfo
	logger                *slog.Logger
	webSocket             *websocket.Conn
	pingTicker            *time.Ticker
	Signal                chan string
	browser               *Browser
	addBindingId          int64
	functionResultChanMap map[int]chan string // TODO 这里有可能会泄露，比如一个方法执行失败了， chan 会永远存在这里，等待chan 的routine 会一直等待
	functionCallId        int
}

func (t *Target) initTarget() error {
	if t.webSocket != nil && t.Signal != nil && t.pingTicker != nil {
		return nil
	}
	t.Close()
	t.logger = logger.NewLogger("target")
	t.addBindingId = addBindingIDMin
	t.functionCallId = functionCallIDMin
	t.functionResultChanMap = make(map[int]chan string)
	err := t.connect()
	t.ping()
	t.listen()
	return err
}
func (t *Target) connect() error {
	t.logger.Info("Connecting to web socket", "url", t.targetInfo.WebSocketDebuggerUrl)
	ws, err := websocket.Dial(t.targetInfo.WebSocketDebuggerUrl, "", t.targetInfo.Url)
	if err != nil {
		t.logger.Error("WebSocket connect fail", "err", err)
		return err
	}
	ws.MaxPayloadBytes = 1024 * 1024 * 100 // 缓冲区太小，websocket 一次接收的数据不全，还要自己缓冲
	t.webSocket = ws
	// 开始这个功能才能使用 Page 相关功能
	_, err = t.webSocket.Write([]byte("{\"method\":\"Page.enable\",\"id\":" + strconv.Itoa(pageEnableId) + " }"))
	//_, err = t.webSocket.Write([]byte("{\"method\":\"Debugger.enable\",\"id\":" + strconv.Itoa(debuggerEnableId) + " }"))
	//t.setInstrumentationBreakpoint("beforeScriptExecution")
	_, err = t.webSocket.Write([]byte("{\"method\":\"Runtime.enable\",\"id\":" + strconv.Itoa(runtimeEnableId) + " }"))
	t.bindFunction(printToPdf, fmt.Sprintf("%s = function(){console.log('print pdf')}", printToPdf))
	return err
}

func (t *Target) Close() {
	if t.webSocket != nil {
		_ = t.webSocket.Close()
	}
	if t.pingTicker != nil {
		t.pingTicker.Stop()
	}
	t.webSocket = nil
	t.Signal = nil
	t.pingTicker = nil
}

func (t *Target) ping() {
	go func() {
		ticker := time.NewTicker(5 * time.Second)
		t.pingTicker = ticker
		defer ticker.Stop()
		for id := 10000; ; id++ {
			msg := fmt.Sprintf("{\"id\":%d, \"method\": \"Browser.getVersion\"}", id)
			t.logger.Debug("ping: " + msg)
			_, writeErr := t.webSocket.Write([]byte(msg))
			if writeErr != nil {
				t.logger.Error("WebSocket ping fail", "err", writeErr)
				_ = t.webSocket.Close()
				break
			}
			if id > 1000000 {
				// 小于 10000 的为特殊用途
				id = 10000
			}
			<-ticker.C
		}
	}()
}

func (t *Target) listen() {
	t.Signal = make(chan string)
	go func() {
		buf := make([]byte, 1024*1024*100) // Runtime 有些数据很大
		n, e := t.webSocket.Read(buf)
		strBf := ""
		for ; e == nil && n > 0; n, e = t.webSocket.Read(buf) {
			_strBf := string(buf[:n]) // todo 可以优化为解析 byte[]
			if !strings.HasSuffix(_strBf, "}") && !strings.HasSuffix(_strBf, "]") {
				strBf = strBf + _strBf
				continue
			} else {
				strBf = strBf + _strBf
				d := &commonData{}
				unmarshalErr := json.Unmarshal([]byte(strBf), d)
				if unmarshalErr != nil {
					//fmt.Println(string(buf[:n]))
					t.logger.Error("unmarshal fail", "err", unmarshalErr, "error_string", string(buf[:n]))
				} else if strings.Compare(InspectorDetached, d.Method) == 0 {
					// chrome 退出了
					t.logger.Info("chrome exit.")
					t.Signal <- InspectorDetached
					t.browser.Close()
				} else if strings.Compare(PageLoaded, d.Method) == 0 {
					// 页面加载 注入一些js
					t.logger.Info("Page loaded")
					t.injectScript(fmt.Sprintf(`const head = document.head;const script = document.createElement('script');script.src = "%s";script.type="text/javascript";head.appendChild(script);`, "http://localhost:"+strconv.Itoa(int(cmdutils.GetServerPort()))+cmdutils.GetRuntimeBaseJs()))
				} else if strings.Compare(DebuggerPaused, d.Method) == 0 {
					// 页面加载 注入一些js
					t.logger.Info("Debugger Paused", "data", strBf)
					t.debuggerResume()
					// 针对一些特殊的debug停止方法， 做特殊处理
					eventParam := d.Params
					eventParamByte, e := json.Marshal(eventParam)
					if e != nil {
						t.logger.Error("Debugger Paused param json marshal fail", "err", e)
					}
					eventData := &debuggerPausedData{}
					e = json.Unmarshal(eventParamByte, eventData)
					if e != nil {
						t.logger.Error("Debugger Paused param json unmarshal fail", "err", e)
					}
					// 在这里生成可以根据方法名进行一些操作，如生成pdf
				} else if strings.Compare(RuntimeBindingCalled, d.Method) == 0 {
					// 在这里生成可以根据方法名进行一些操作，如生成pdf, 这里会比上面的 Debugger.paused 事件好，因为那个开启后会特别消耗内存
					eventParam := d.Params
					eventParamByte, e := json.Marshal(eventParam)
					if e != nil {
						t.logger.Error("binding called param json marshal fail", "err", e)
					}
					eventData := &bindingCalledParam{}
					e = json.Unmarshal(eventParamByte, eventData)
					if e != nil {
						t.logger.Error("binding called param json unmarshal fail", "err", e)
					}
					if strings.Compare(eventData.Name, printToPdf) == 0 {
						t.printToPdf()
					}
				} else {
					if d.Id != 0 && t.functionResultChanMap[int(d.Id)] != nil {
						// 方法调用的返回值
						result := &commonResultData{}
						unmarshalErr = json.Unmarshal([]byte(strBf), result)
						if unmarshalErr != nil {
							t.logger.Error("method result unmarshal fail", "id", d.Id, "err", unmarshalErr)
							t.functionResultChanMap[int(d.Id)] = nil
							continue
						}

						ch := t.functionResultChanMap[int(d.Id)]
						t.functionResultChanMap[int(d.Id)] = nil
						arr, _ := json.Marshal(result.Result)
						ch <- string(arr) // todo 这里多少有点浪费内存
					} else {
						t.logger.Debug("read message ", "message", strBf)
					}
				}
				strBf = ""
			}
		}
	}()
}

func (t *Target) injectScript(script string) {
	ws := t.webSocket

	data := &commonData{}
	params := &runtimeEvaluateParams{Expression: script}
	data.Params = params
	data.Method = "Runtime.evaluate"
	data.Id = injectScriptId

	dataStr, err := json.Marshal(data)
	if err != nil {
		t.logger.Error("parse injectScript", "error", err.Error())
		return
	}
	t.logger.Info("parse injectScript", "data", string(dataStr))
	_, err = ws.Write(dataStr)
	if err != nil {
		t.logger.Error("write injectScript", "error", err.Error())
	}
}

func (t *Target) bindFunction(functionName string, source string) {
	ws := t.webSocket
	t.addBindingId++
	// 先不考虑并发，因为在这个 target 中 这个方法不会被并发调用
	if t.addBindingId > addBindingIDMax {
		t.addBindingId = addBindingIDMin
	}
	data := &commonData{}
	data.Id = t.addBindingId
	params := &addBinding{Name: functionName}
	data.Params = params
	data.Method = "Runtime.addBinding"

	dataStr, err := json.Marshal(data)
	if err != nil {
		t.logger.Error("add binding", "error", err.Error())
		return
	}
	t.logger.Info("add binding", "data", string(dataStr))
	_, err = ws.Write(dataStr)
	if err != nil {
		t.logger.Error("add binding", "error", err.Error())
	}

	t.injectScript(source)
}

// instrumentation beforeScriptExecution 或 beforeScriptWithSourceMapExecution
func (t *Target) setInstrumentationBreakpoint(instrumentation string) {
	ws := t.webSocket

	data := &commonData{}
	data.Id = setInstrumentationBreakpointID
	params := map[string]interface{}{
		"instrumentation": instrumentation,
	}
	data.Params = params
	data.Method = "Debugger.setInstrumentationBreakpoint"

	dataStr, err := json.Marshal(data)

	if err != nil {
		t.logger.Error("setInstrumentationBreakpoint", "error", err.Error())
		return
	}
	t.logger.Info("setInstrumentationBreakpoint", "data", string(dataStr))
	_, err = ws.Write(dataStr)
	if err != nil {
		t.logger.Error("setInstrumentationBreakpoint", "error", err.Error())
	}
}

func (t *Target) debuggerResume() {
	ws := t.webSocket

	data := &commonData{}
	data.Id = debugResume
	params := map[string]interface{}{}
	data.Params = params
	data.Method = "Debugger.resume"

	dataStr, err := json.Marshal(data)

	if err != nil {
		t.logger.Error("Debugger.resume", "error", err.Error())
		return
	}
	t.logger.Info("Debugger.resume", "data", string(dataStr))
	_, err = ws.Write(dataStr)
	if err != nil {
		t.logger.Error("Debugger.resume", "error", err.Error())
	}
}

func (t *Target) printToPdf() {
	ws := t.webSocket

	data := &commonData{}
	t.functionCallId++
	id := t.functionCallId
	if id > functionCallIDMax {
		t.functionCallId = functionCallIDMin
		id = functionCallIDMax
	}

	data.Id = int64(id)
	params := printToPdfParam{
		Landscape: false, MarginLeft: 0, MarginTop: 0, MarginBottom: 0, MarginRight: 0, DisplayHeaderFooter: false, PrintBackground: true,
	}
	data.Params = params
	data.Method = "Page.printToPDF"

	dataStr, err := json.Marshal(data)

	if err != nil {
		t.logger.Error("Page.printToPDF", "error", err.Error())
		return
	}
	t.logger.Info("Page.printToPDF", "data", string(dataStr))
	_, err = ws.Write(dataStr)
	if err != nil {
		t.logger.Error("Page.printToPDF", "error", err.Error())
	}
	pdfCh := make(chan string)
	t.functionResultChanMap[id] = pdfCh
	go func() {
		t.logger.Info("start print to pdf.")
		pdfStr := <-pdfCh
		pdfData := &printToPdfResult{}
		e := json.Unmarshal([]byte(pdfStr), pdfData)
		if e != nil {
			t.logger.Error("pdf decode result json fail", "err", e)
			return
		}
		t.logger.Info("receive pdf.", "size", len(pdfStr))
		base64Str := pdfData.Data
		pdfBytes, err := base64.StdEncoding.DecodeString(base64Str)
		if err != nil {
			t.logger.Error("pdf decode base64 fail", "err", e)
			return
		}
		path := cmdutils.GetProgramDir()
		pdfPath := filepath.Join(path, "tmp", strconv.Itoa(rand.Int())+".pdf")
		err = os.WriteFile(pdfPath, pdfBytes, 0644)
		if err != nil {
			t.logger.Error("write to file error.", "error", err)
		}
	}()
}

type commonData struct {
	Method string      `json:"method"`
	Id     int64       `json:"id"`
	Params interface{} `json:"params"`
}

type commonResultData struct {
	commonData
	Result interface{} `json:"result"`
}

// debuggerPausedData Debugger.paused  事件的 params
type debuggerPausedData struct {
	CallFrames []callFrame `json:"callFrames"`
	Reason     string      `json:"reason"`
}

type callFrame struct {
	CallFrameId  string `json:"callFrameId"`
	FunctionName string `json:"functionName"`
}

// Runtime.evaluate 的参数
type runtimeEvaluateParams struct {
	Expression string `json:"expression"`
}

type addBinding struct {
	Name                 string `json:"name"`
	ExecutionContextId   string `json:"executionContextId,omitempty"`
	ExecutionContextName string `json:"executionContextName,omitempty"`
}

type printToPdfParam struct {
	Landscape           bool    `json:"landscape,omitempty"`
	DisplayHeaderFooter bool    `json:"displayHeaderFooter,omitempty"`
	PrintBackground     bool    `json:"printBackground,omitempty"`
	Scale               bool    `json:"scale,omitempty"`
	PaperWidth          float32 `json:"paperWidth,omitempty"`
	PaperHeight         float32 `json:"paperHeight,omitempty"`
	MarginTop           float32 `json:"marginTop"`
	MarginBottom        float32 `json:"marginBottom"`
	MarginLeft          float32 `json:"marginLeft"`
	MarginRight         float32 `json:"marginRight"`
	PageRanges          string  `json:"pageRanges,omitempty"`
}
type bindingCalledParam struct {
	Name    string `json:"name"`
	Payload string `json:"payload"`
}

type printToPdfResult struct {
	Data string `json:"data"`
}

func (c commonData) String() string {
	return fmt.Sprintf("id: %d method: %s params: %v", c.Id, c.Method, c.Params)
}

type TargetInfo struct {
	Description          string `json:"description"`
	DevtoolsFrontendUrl  string `json:"devtoolsFrontendUrl"`
	FaviconUrl           string `json:"faviconUrl"`
	Id                   string `json:"id"`
	Title                string `json:"title"`
	Url                  string `json:"url"`
	WebSocketDebuggerUrl string `json:"webSocketDebuggerUrl"`
	TargetType           string `json:"type"`
}

func CloneTargetInfo(targetInfo *TargetInfo) TargetInfo {
	return TargetInfo{
		Description:          targetInfo.Description,
		DevtoolsFrontendUrl:  targetInfo.DevtoolsFrontendUrl,
		FaviconUrl:           targetInfo.FaviconUrl,
		Id:                   targetInfo.Id,
		Title:                targetInfo.Title,
		Url:                  targetInfo.Url,
		WebSocketDebuggerUrl: targetInfo.WebSocketDebuggerUrl,
		TargetType:           targetInfo.TargetType,
	}

}
